<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

/**
 * A SpicaNavigationPath is a software component which is used to generate HTML code
 * to render navigation links in a form of a bread crumb hierarchy. By default,
 * it renders a horizontal list from left to right (oldest left) with bread crumb
 * links and a ' > ' as a seperator, e.g
 *
 *   Home > Products & Solutions > Hardware > Desktop Systems
 *
 * This class is partially ported from Pone_Action_NavigationPath (PONE framework).
 *
 * @category   spica
 * @package    core
 * @subpackage view
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      May 05, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: NavigationPath.php 1869 2011-01-07 18:55:25Z pcdinh $
 */

/**
 * How-to
 *
 * @example
 *
 * $breadcrumb = new SpicaNavigationPath();
 * $breadcrumb->addNode('home', 'Home page', 'home');
 * $breadcrumb->addNode('home_news', 'News', 'home/news');
 * $breadcrumb->addNode('home_news_categories', 'News categories', 'home/news/categories');
 * $breadcrumb->addNode('home_news_category_sport', 'Sport', 'home/news/categories/sport');
 * $breadcrumb->addNode('home_news_category_tech', 'Technology', 'home/news/categories/tech');
 * $breadcrumb->addNode('home_news_mostread', 'Sport', 'home/news/mostread');
 *
 * $breadcrumb->setParentNode('home_news_category_tech', 'home_news_categories');
 * $breadcrumb->setParentNode('home_news_category_sport', 'home_news_categories');
 * $breadcrumb->setParentNode('home_news_categories', 'home_news');
 * $breadcrumb->setParentNode('home_news', 'home');
 * $breadcrumb->setParentNode('home_news_mostread', 'home_news');
 * echo $breadcrumb->getPathByUrl('home/news/categories/tech');
 *
 * @see http://developer.yahoo.com/ypatterns/pattern.php?pattern=breadcrumbs
 */
class SpicaNavigationPath
{
    /**
     * An array of nodes in breadcrumb
     *
     * @var array
     */
    protected $_nodes = array();

    /**
     * Active node name
     *
     * This node name is internally used by SpicaNavigationPath::getPath()
     *
     * @var string
     */
    protected $_activeNodeName;

    /**
     * An array that store urls as keys and node names as values
     *
     * @var array
     */
    protected $_urls  = array();

    /**
     * Breadcrumb's URL delimeter string
     *
     * @var string
     */
    protected $_delimiter = '&raquo;';

    /**
     * Constructs an object of <code>SpicaNavigationPath</code>
     *
     */
    public function __construct()
    {

    }

    /**
     * Adds a node to breadcrumb
     *
     * @param string $name
     * @param string $title
     * @param string $url
     */
    public function addNode($name, $title, $url)
    {
        $node                = new SpicaNavigationNode();
        $node->title         = $title;
        $node->url           = $url;
        $node->parent        = null;
        $this->_nodes[$name] = & $node;
        // Fast lookup by URL
        $this->_urls[$url]   = $name;
    }

    /**
     * Sets breadcrumb delimiter
     *
     * @param  string $delimiter
     * @return void
     */
    public function setDelimiter($delimiter)
    {
        $this->_delimiter = (string) $delimiter;
    }

    /**
     * Sets active node name
     *
     * Active node name is set manually to allow developer specify the exact current node name that need being found the path back to the root
     *
     * Normally, the current url is used to determine which node name is. If the active node name is specified, the current url is just ignored
     *
     * @param string $name
     */
    public function setActiveNodeName($name)
    {
        $this->_activeNodeName = $name;
    }

    /**
     * Gets node name that is associated with an url
     *
     * @param  string $url
     * @return string
     */
    public function getNodeNameByUrl($url)
    {
        if (true === isset($this->_urls[$url]))
        {
            return $this->_urls[$url];
        }

        return null;
    }

    /**
     * Sets parent node for a certain node name
     *
     * @throws OutOfRangeException      When child node or parent node does not exist.
     *         InvalidArgumentException When child node and parent node is the same
     * @param  string $childNodeName
     * @param  string $parentNodeName
     */
    public function setParentNode($childNodeName, $parentNodeName)
    {
        if ($childNodeName === $parentNodeName)
        {
            throw new InvalidArgumentException('Child node name must be different than parent node name');
        }

        if (false === isset($this->_nodes[$childNodeName]))
        {
            throw new OutOfRangeException('Child node named '.$childNodeName. ' does not exist');
        }

        if (false === isset($this->_nodes[$parentNodeName]))
        {
            throw new OutOfRangeException('Parent node named '.$parentNodeName. ' does not exist');
        }

        $this->_nodes[$childNodeName]->parent = & $this->_nodes[$parentNodeName];
    }

    /**
     * Gets return path to root in an array of nodes
     *
     * @param  string $nodeName
     * @param  bool   $rootLinkable Defaults to false
     * @param  bool   $currentNodeLinkable Defaults to false
     * @return array
     */
    public function getPathToRoot($nodeName, $rootLinkable = false, $currentNodeLinkable = false)
    {
        $path   = array();
        // Avoid passing by reference
        $node   = clone $this->_nodes[$nodeName];
        $url    = $this->_nodeLink($node, (null === $node->parent)?$rootLinkable:$currentNodeLinkable);
        $path[] = $url;

        while (null    !== $node)
        {
            $parentNode = $node->parent;

            if (null   !== $parentNode)
            {
                $path[] = $this->_nodeLink($parentNode, (null === $parentNode->parent)?$rootLinkable:true);
            }

            // Change current node
            $node = $parentNode;
        }

        return array_reverse($path);
    }

    /**
     * Gets path by url
     *
     * @param  string $url The destination URL (current page URL)
     * @param  bool   $rootLinkable Defaults to false
     * @param  bool   $currentNodeLinkable Defaults to false
     * @return string
     */
    public function getPathByUrl($url, $rootLinkable = false, $currentNodeLinkable = false)
    {
        $nodeName = $this->getNodeNameByUrl($url);

        $url      = '';

        if (null !== $nodeName)
        {
            $path   = $this->getPathToRoot($nodeName, $rootLinkable, $currentNodeLinkable);

            $length = count($path);

            if ($length > 0)
            {
                $url = implode(' '.$this->_delimiter.' ', $path);
            }
        }

        return $url;
    }

    /**
     * Returns breadcrumb navagation path based on the current request object
     *
     * @return string
     */
    public function getPath()
    {
        if (null === $this->_activeNodeName)
        {
            $url = SpicaRequest::getModule().'/'.SpicaRequest::getController().'/'.SpicaRequest::getAction();
            return $this->getPathByUrl($url);
        }

        return $this->getPathToRoot($this->_activeNodeName, false, false);
    }

    /**
     * Generates node link in breadcrumb
     *
     * @param  SpicaNavigationNode $node
     * @param  bool                 $rootLinkable
     * @return string
     */
    private function _nodeLink($node, $linkable = false)
    {
        return (true === $linkable)?' <a href="'.$node->url.'">'.$node->title.'</a>':' '.$node->title;
    }
}

/**
 * Each breadcrumb node represents a separated link in the breadcrumb (or homeward)
 * navigation path
 *
 * @category   spica
 * @package    core
 * @subpackage view
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      May 05, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: NavigationPath.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaNavigationNode
{
    /**
     * Node URL
     *
     * @var string
     */
    public $url;

    /**
     * Link title
     *
     * @var string
     */
    public $title;

    /**
     * Parent node
     *
     * @var SpicaNavigationNode
     */
    public $parent;
}

?>